package com.google.code.mergeinformer.service.impl;

import com.google.code.mergeinformer.model.Revision;
import com.google.code.mergeinformer.model.Task;
import com.google.code.mergeinformer.service.TaskService;
import com.google.code.mergeinformer.service.VcsService;
import com.google.code.mergeinformer.util.synchronization.MutexProvider;
import org.apache.commons.lang.exception.ExceptionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.persistence.NoResultException;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;

/**
 * @author Dmitry Golubev
 */
@Service
public class TaskServiceImpl extends ServiceBaseImpl<Task> implements TaskService {

    private static final Logger log = LoggerFactory.getLogger(TaskServiceImpl.class);

    private final MutexProvider<String, Operation> mutexProvider = new MutexProvider<>();
    private final Map<String, ScheduledFuture> scheduledTasks = new ConcurrentHashMap<>();

    @Autowired
    private TaskScheduler taskScheduler;
    @Autowired
    private VcsService vcsService;

    @Override
    public Task getByShortName(String projectShortName, String taskShortName) {
        try {
            return em.createNamedQuery("Task.byShortNames", Task.class)
                    .setParameter("projectShortName", projectShortName)
                    .setParameter("taskShortName", taskShortName).getSingleResult();
        } catch (NoResultException e) {
            return null;
        }
    }

    @Override
    public void schedule(Task task) {
        synchronized (mutexProvider.getMutex(task.getId(), Operation.SCHEDULE)) {
            unschedule(task);
            final ScheduledFuture scheduledFuture
                    = taskScheduler.scheduleWithFixedDelay(getRunnable(task), task.getExecutionInterval() * 1000);
            scheduledTasks.put(task.getId(), scheduledFuture);
        }
    }

    @Override
    public void scheduleDelayed(Task task) {
        synchronized (mutexProvider.getMutex(task.getId(), Operation.SCHEDULE)) {
            unschedule(task);
            final Calendar calendar = Calendar.getInstance();
            calendar.add(Calendar.SECOND, task.getExecutionInterval().intValue());
            final ScheduledFuture scheduledFuture =
                    taskScheduler.scheduleWithFixedDelay(getRunnable(task), calendar.getTime(), task.getExecutionInterval() * 1000);
            scheduledTasks.put(task.getId(), scheduledFuture);
        }
    }

    @Override
    public boolean unschedule(Task task) {
        synchronized (mutexProvider.getMutex(task.getId(), Operation.SCHEDULE)) {
            final ScheduledFuture removed = scheduledTasks.remove(task.getId());
            return removed != null && removed.cancel(false);
        }
    }

    @Override
    public Task execute(final Task task) {
        // perform extrusive execution
        mutexProvider.executeExtrusive(new MutexProvider.ExtrusiveBlock() {
            @Override
            public void execute() {
                unschedule(task);
                try {
                    executeInternal(task);
                } finally {
                    scheduleDelayed(task);
                }
            }
        }, task.getId(), Operation.SCHEDULE, Operation.EXECUTE);
        return find(task.getId());
    }

    @Override
    public boolean isShortNameExists(String projectShortName, String takShortName) {
        return em.createNamedQuery("Task.isShortNameBusy", Long.class)
                .setParameter("projectShortName", projectShortName)
                .setParameter("taskShortName", takShortName).getSingleResult() == 1;
    }

    @Override
    public boolean isNameExists(String projectShortName, String taskName) {
        return em.createNamedQuery("Task.isNameBusy", Long.class)
                .setParameter("projectShortName", projectShortName)
                .setParameter("taskName", taskName).getSingleResult() == 1;
    }

    private Runnable getRunnable(final Task task) {
        return new Runnable() {
            @Override
            public void run() {
                final Task executedTask = executeInternal(task);
                if (executedTask == null) {
                    log.info("Unscheduling deleted Task id=" + task.getId());
                    // throw away phantom task
                    unschedule(task);
                }
            }
        };
    }

    @Transactional
    private Task executeInternal(Task task) {
        final Task loadedTask = find(task.getId());
        if (loadedTask != null) {
            // perform extrusive execution
            mutexProvider.executeExtrusive(new MutexProvider.ExtrusiveBlock() {
                @Override
                public void execute() {
                    try {
                        final List<Revision> unmergedRevisions = vcsService.getUnmergedRevisions(loadedTask);
                        loadedTask.replaceRevisions(unmergedRevisions);
                        loadedTask.setStackTrace(null);
                        loadedTask.setExecuted(new Date());
                    } catch (Exception e) {
                        log.error("An error has occurred during execution of the Task id=" + loadedTask.getId(), e);
                        loadedTask.setStackTrace(ExceptionUtils.getStackTrace(e));
                    }
                }
            }, task.getId(), Operation.EXECUTE);
        }
        return loadedTask;
    }

    private static enum Operation {
        EXECUTE, SCHEDULE
    }
}
